using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using GsmComm.GsmCommunication;
using GsmComm.PduConverter;
using GsmComm.PduConverter.SmartMessaging;
using Lap.Collections;
using Lap.Extensions.Core;
using PpSmsSender.Db;

namespace PpSmsSender
{
    public class ModemHandler : IDisposable
    {
        public delegate void ReportEventHandler(object sender, string msg);

        private readonly string _name;

        private readonly ConcurrentDictionary<long, SmsMessage> _pendingMessagesWaitForStatus = new ConcurrentDictionary<long, SmsMessage>();
        private readonly ConcurrentDictionary<int, ConcurrentQueue<SmsPdu>> _pendingMultipartRecieved = new ConcurrentDictionary<int, ConcurrentQueue<SmsPdu>>();

        private readonly ConcurrentPriorityQueue<SmsMessage> _incomingMessages = new ConcurrentPriorityQueue<SmsMessage>(16);
        public ConcurrentQueue<SmsMessage> PerformedCommands = new ConcurrentQueue<SmsMessage>();
        public ConcurrentQueue<SmsMessage> RecievedMessages = new ConcurrentQueue<SmsMessage>();
        public ConcurrentQueue<SmsMessage> OperatorMessages = new ConcurrentQueue<SmsMessage>();
        public ConcurrentQueue<SmsMessage> RecievedInternalMessages = new ConcurrentQueue<SmsMessage>();
        public ConcurrentQueue<SmsMessage> FailedToSendMessages = new ConcurrentQueue<SmsMessage>();
        public ConcurrentQueue<SmsMessage> SentMessages = new ConcurrentQueue<SmsMessage>();
        public ConcurrentQueue<RecievedMessageStatus> LateStatus = new ConcurrentQueue<RecievedMessageStatus>();

        public DateTime PhoneLastDisconnected;
        private DateTime _lastSuccessfulSend;
        private DateTime _lastSent;
        private volatile uint _pendingSentIndexCounter;
        private volatile bool _phoneDisconnected;
        private volatile int _waitingSendAckCounter;
        internal GsmCommMain _comm;

        private IConfigModemReadOnly _config;
        private int _sentMessages;

        private PollingThread _eventLoop;
        private SmsSender _parent;
        private volatile bool _isSending;

        public event ReportEventHandler Report;

        public ModemHandler(string name, SmsSender smsSender)
        {
            this._name = name;
            this._parent = smsSender;
        }

        public DateTime LastAttemptedSend
        {
            get { return this._lastSent; }
        }

        public DateTime LastSuccessfulSend
        {
            get { return _lastSuccessfulSend; }
        }


        public PollingThread.State ThreadState
        {
            get { return this._eventLoop == null ? PollingThread.State.Stopped : _eventLoop.ThreadState; }
        }

        public bool IsReciveOnly
        {
            get { return _phoneDisconnected || this.Config.IsPaused || this.Config.IsRecieveOnly || !this._comm.IsConnected() || !_comm.IsOpen(); }
        }


        public int SentMessageCount
        {
            get
            {
                return (this.Config == null ? this._sentMessages : this.Config.SentSinceFilled);
            }

        }
        private void IncrementNumberOfSent()
        {
            _sentMessages++;
            this.Config.IncrementSentSinceFilled(1);
        }


        public IConfigModemReadOnly Config
        {
            get { return _config; }
            set
            {
                int tmp = 0;
                if (_config == null)
                {
                    tmp = this._sentMessages;
                }
                _config = value;
                Config.IncrementSentSinceFilled(tmp);
            }
        }

        public string Name
        {
            get { return _name; }
        }

        public int SendQueueLength
        {
            get { return this._incomingMessages.PrioRange.Sum(p => this._incomingMessages[p].Count); }
        }

        public bool Connected
        {
            get { return this._comm != null && this._comm.IsConnected(); }
        }

        private GsmCommMain CreateGsmCommMain(string portName, int baudRate, int timeout)
        {
            GsmCommMain comm = new GsmCommMain(portName, baudRate, timeout);
            try
            {
                comm.Open();
                comm.EnablePermanentSmsBatchMode();
                comm.MessageReceived += this.comm_MessageReceived;
                comm.MessageSendComplete += this.comm_MessageSendComplete;
                comm.MessageSendFailed += this.comm_MessageSendFailed;
                comm.PhoneDisconnected += (sender, e) =>
                                              {
                                                  PhoneLastDisconnected = DateTime.Now;
                                                  this._phoneDisconnected = true;
                                              };
                comm.PhoneConnected += (sender1, e1) => this._phoneDisconnected = false;
                comm.DisableMessageRouting();
                comm.EnableMessageNotifications();

                if (!comm.IsConnected())
                {
                    throw new Exception("Can't connect to modem on " + portName + " in CreateGsmCommMain");
                }
                this.Output("Modem connected to " + this.Name);
                return comm;
            }
            catch (Exception ex)
            {
                comm.Close();
                throw new Exception("Error when setting up modem:" + ex.Message, ex);
            }
        }

        private void comm_MessageSendFailed(object sender, MessageErrorEventArgs e)
        {
            this.Output("comm_MessageSendFailed");
            this._waitingSendAckCounter--;
            try
            {
                SmsSubmitPdu statusPdu = e.Pdu as SmsSubmitPdu;
                if (statusPdu != null)
                {
                    string no = statusPdu.DestinationAddress;
                    byte refbyte = statusPdu.MessageReference;
                    this.Output("failed: " + no + "(" + refbyte + ")");
                    while (this._isSending)
                        Thread.Sleep(50);

                    foreach (long pendingkey in this._pendingMessagesWaitForStatus.Keys)
                    {
                        SmsMessage pendingMessage;
                        this._pendingMessagesWaitForStatus.TryGetValue(pendingkey, out pendingMessage);
                        if (pendingMessage.Number == no && pendingMessage.Reference == refbyte)
                        {

                            var oldStatus = pendingMessage.Status;
                            pendingMessage.Status = (int)SmsMessage.Statuses.SendFailed;
                            pendingMessage.FailCounter++;
                            pendingMessage.Result = "Recieved at " + statusPdu.GetTimestamp().ToDateTime().ToString("yyyy-MM-dd HH:MM:ss") + "Error=" + e.Exception.Message + "\r\n" + statusPdu.UserData;
                            _parent.MessageStatusChanged(pendingMessage, oldStatus);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                this.ShowException(ex);
            }
        }

        private void comm_MessageSendComplete(object sender, MessageEventArgs e)
        {
            this.Output("comm_MessageSendComplete");
            this._waitingSendAckCounter--;
            try
            {
                var sentPdu = e.Pdu as SmsSubmitPdu;
                if (sentPdu != null)
                {
                    this._lastSuccessfulSend = DateTime.Now;
                    string no = sentPdu.DestinationAddress;
                    byte refbyte = sentPdu.MessageReference;
                    while (this._isSending)
                        Thread.Sleep(50);
                    foreach (long pendingkey in this._pendingMessagesWaitForStatus.Keys)
                    {
                        SmsMessage pendingMessage;
                        this._pendingMessagesWaitForStatus.TryGetValue(pendingkey, out pendingMessage);
                        if (pendingMessage.Number == no && pendingMessage.Reference == refbyte)
                        {
                            var oldStatus = pendingMessage.Status;
                            pendingMessage.Status = (int)SmsMessage.Statuses.SendDone;
                            pendingMessage.SentAt = DateTime.Now;
                            pendingMessage.Result = "Sent at " + pendingMessage.SentAt.GetValueOrDefault().ToString("yyyy-MM-dd HH:MM:ss") + Calc.IntToHex(sentPdu.UserData);
                            _parent.MessageStatusChanged(pendingMessage, oldStatus);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                this.ShowException(ex);
            }
        }
        public void SendMessage(SmsMessage msg)
        {
            if (!_eventLoop.IsCurrentThread)
            {
                this._incomingMessages[msg.Priority].Enqueue(msg);
                return;
            }
            string text = msg.Text;
            string number = msg.Number;

            SmsSubmitPdu[] pdus;
            try
            {
                pdus = SmartMessageFactory.CreateConcatTextMessage(text, NeedUnicode(text), number);
            }
            catch (Exception ex)
            {
                this.ShowException(ex);
                pdus = new SmsSubmitPdu[0];
            }

            if (pdus.Length == 0)
            {
                this.Output("Error: No PDU parts have been created!");
                this.Output("");
            }

            if (pdus.Length > 0)
            {
                try
                {
                    // Send the created messages
                    byte lastRefloop = 0;
                    pdus
                        .ForEach(
                            (pdu, ix) =>
                            {
                                if (this._comm == null)
                                {
                                    this.Output("Sending with no modem connected");
                                }
                                else
                                {
                                    if (ix == pdus.Length - 1)
                                    {
                                        pdu.RequestStatusReport = true;
                                    }
                                    this._lastSent = DateTime.Now;
                                    msg.Status = (int)SmsMessage.Statuses.Sent;
                                    msg.SentAt = DateTime.Now;
                                    msg.RecievedBy = this.Config.PhoneNumber.ToString();
                                    this._isSending = true;
                                    ++this._waitingSendAckCounter;
                                    var oldCounter = this._waitingSendAckCounter;
                                    this._comm.SendMessage(pdu);
                                    if (ix == pdus.Length - 1)
                                    {
                                        msg.Reference = lastRefloop = pdu.MessageReference;
                                        this._pendingMessagesWaitForStatus.TryAdd(++this._pendingSentIndexCounter, msg);
                                    }
                                    IncrementNumberOfSent();
                                    var loopout = 0;
                                    while (this._waitingSendAckCounter == oldCounter && ++loopout < 100)
                                        Thread.Sleep(50);
                                    this._isSending = false;
                                }
                            });
                    this.Output("Sent to " + msg.Number + "(" + lastRefloop + ") " + ":" + msg.Text);
                }
                catch (Exception ex)
                {
                    --this._waitingSendAckCounter;
                    msg.Result += ex.Message + ";";
                    this.ShowException(ex);
                    this.Output("Message sending aborted because of an error." + ex.Message + "\r\n" + ex.StackTrace);
                    msg.Status = (int)SmsMessage.Statuses.SendException;
                    msg.FailCounter++;
                    throw new Exception("Failed to send.", ex);
                }
            }
        }



        private void comm_MessageReceived(object sender, MessageReceivedEventArgs e)
        {
            try
            {
                this.Output("comm_MessageReceived");

                IMessageIndicationObject obj = e.IndicationObject;
                MemoryLocation loc = obj as MemoryLocation;
                ShortMessage msg = obj as ShortMessage;
                SmsPdu pdu = null;
                if (loc != null)
                {
                    DecodedShortMessage x = this._comm.ReadMessage(loc.Index, loc.Storage);
                    pdu = x.Data;
                    this._comm.DeleteMessage(loc.Index, loc.Storage);
                }

                if (msg != null)
                {   //Routed message
                    if (this._comm.IsAcknowledgeRequired())
                    {
                        this._comm.AcknowledgeNewMessage();
                    }
                    pdu = this._comm.DecodeReceivedMessage(msg);
                }

                if (this.ProcessRecievedPdu(pdu))
                    return;

                this.Output("Error: Unknown notification object!");
            }
            catch (Exception ex)
            {
                this.ShowException(ex);
            }
        }

        public void EnqueueStoredMessages(PhoneMessageStatus? phoneMessageStatus = null, string storage = null)
        {
            if (storage == null)
            {
                EnqueueStoredMessages(phoneMessageStatus, PhoneStorageType.Phone);
                EnqueueStoredMessages(phoneMessageStatus, PhoneStorageType.Sim);
            }
            else
            {
                if (!phoneMessageStatus.HasValue)
                {
                    EnqueueStoredMessages(PhoneMessageStatus.ReceivedRead, storage);
                    EnqueueStoredMessages(PhoneMessageStatus.ReceivedUnread, storage);
                }
                else
                {
                    try
                    {
                        var tempstack = new Stack<SmsPdu>();
                        // Read all SMS messages from the storage
                        DecodedShortMessage[] messages = this._comm.ReadMessages(phoneMessageStatus.Value, storage);

                        foreach (DecodedShortMessage message in messages.OrderByDescending(m => m.Index))
                        {
                            this._comm.DeleteMessage(message.Index, message.Storage);
                            SmsPdu smsPdu = message.Data;
                            tempstack.Push(smsPdu);
                        }
                        if (messages.Length > 0)
                            this.Output(string.Format("{0,5} {1} messages read from {2}.", messages.Length, phoneMessageStatus.Value, storage));
                        while (tempstack.Count > 0)
                            this.ProcessRecievedPdu(tempstack.Pop());

                    }
                    catch (Exception ex)
                    {
                        this.ShowException(ex);
                    }
                }
            }

        }


        private bool ProcessRecievedPdu(SmsPdu pdu)
        {
            try
            {
                SmsStatusReportPdu statusPdu = pdu as SmsStatusReportPdu;
                if (statusPdu != null)
                {

                    PhoneNumber no = statusPdu.RecipientAddress;
                    byte refbyte = statusPdu.MessageReference;
                    this.Output("SmsStatusReportPdu recieved " + no + "(" + refbyte + ") " + statusPdu.Status.ToString());

                    bool found = false;
                    foreach (var pendingkey in this._pendingMessagesWaitForStatus.Keys)
                    {
                        SmsMessage pendingMessage;
                        this._pendingMessagesWaitForStatus.TryGetValue(pendingkey, out pendingMessage);

                        if (pendingMessage.Number.ToString() == no.ToString() && pendingMessage.Reference == refbyte)
                        {
                            var oldStatus = pendingMessage.Status;
                            pendingMessage.Status = statusPdu.Status.ToByte();
                            pendingMessage.Result = "Recieved at " + statusPdu.GetTimestamp().ToDateTime().ToString("yyyy-MM-dd HH:MM:ss") + " Status=" + statusPdu.Status.ToString() + " \r\n" + statusPdu.UserData;
                            pendingMessage.RecievedAt = statusPdu.GetTimestamp().ToDateTime();
                            found = true;
                            _parent.MessageStatusChanged(pendingMessage, oldStatus);
                            break;
                        }
                    }
                    if (!found)
                    {
                        var stat = new RecievedMessageStatus
                        {
                            Number = no,
                            Refbyte = refbyte,
                            Status = statusPdu.Status,
                            RecievedAt = statusPdu.GetTimestamp().ToDateTime()
                        };
                        this._parent.RecievedStatuses.Enqueue(stat);
                        this.Output("Late notification for: " + stat.Number + "(" + stat.Refbyte + ")");

                    }
                    return true;
                }

                SmsDeliverPdu deliverPdu = pdu as SmsDeliverPdu;
                if (deliverPdu != null)
                {
                    SmsMessage sms = null;
                    if (SmartMessageDecoder.IsPartOfConcatMessage(deliverPdu))
                    {
                        IConcatenationInfo concatInfo = SmartMessageDecoder.GetConcatenationInfo(deliverPdu);
                        int refNo = concatInfo.ReferenceNumber;
                        if (!_pendingMultipartRecieved.ContainsKey(refNo))
                            this._pendingMultipartRecieved[refNo] = new ConcurrentQueue<SmsPdu>();
                        ConcurrentQueue<SmsPdu> concurrentQueue = this._pendingMultipartRecieved[refNo];
                        concurrentQueue.Enqueue(deliverPdu);
                        if (concurrentQueue.Count == concatInfo.TotalMessages)
                        {
                            ConcurrentQueue<SmsPdu> bag;
                            this._pendingMultipartRecieved.TryRemove(refNo, out bag);
                            var pdus = bag.ToArray();
                            string fullmessage = MySmartMessageDecoder.CombineConcatMessageText(pdus);
                            sms = new SmsMessage
                                      {
                                          Text = fullmessage,
                                          Number = deliverPdu.OriginatingAddress,
                                          RecievedAt = pdus[0].GetTimestamp().ToDateTime(),
                                          RecievedBy = this.Config.PhoneNumber,
                                          Type = SmsMessage.Types.Recieved
                                      };

                            _parent.MessageStatusChanged(sms, 0);

                            this.Output("New multi message received:");
                        }
                    }
                    else
                    {
                        sms = new SmsMessage
                                  {
                                      Text = deliverPdu.UserDataText,
                                      Number = deliverPdu.OriginatingAddress,
                                      RecievedAt = deliverPdu.GetTimestamp().ToDateTime(),
                                      RecievedBy = this.Config.PhoneNumber,
                                      Type = SmsMessage.Types.Recieved

                                  };
                        _parent.MessageStatusChanged(sms, 0);

                        this.Output("New simple message received:");
                    }

                    if (sms != null)
                    {
                        if ((this.Config != null
                            && sms.Number.Number.ToLower() == this.Config.Sender.ToLower()))
                            this.OperatorMessages.Enqueue(sms);
                        else if (sms.Text.StartsWith(SmsSender.InternalMessageDesignator))
                            this.RecievedInternalMessages.Enqueue(sms);
                        else
                            this._parent.RecievedMessages.Enqueue(sms);
                    }

                    return true;
                }

            }
            catch (Exception ex)
            {
                this.ShowException(ex);
            }
            return false;
        }

        public void Start()
        {
            if (this.ThreadState == PollingThread.State.Running)
            {
                throw new InvalidAsynchronousStateException("Already running");
            }

            this.EnqueueStoredMessages();

            this._eventLoop =
                new PollingThread(thread =>
                                      {
                                          this.FinalizePendingForStatus();
                                          if (this._waitingSendAckCounter < 1 && _isSending == false)
                                          {
                                              foreach (var prio in this._incomingMessages.PrioRange)
                                              {
                                                  SmsMessage incomingMessage;
                                                  while (this._incomingMessages[prio].TryDequeue(out incomingMessage))
                                                  {
                                                      try
                                                      {
                                                          this.SendMessage(incomingMessage);
                                                          var pause = _parent.PauseAfterSend;
                                                          Thread.Sleep(pause);
                                                          return false;
                                                      }
                                                      catch (Exception ex)
                                                      {
                                                          _parent.FailedSendMessages.Enqueue(incomingMessage);
                                                      }
                                                  }
                                              }
                                          }
                                          return false;
                                      }, this._name, 200);
            this._eventLoop.Start();

        }


        public void Stop()
        {
            if (this.ThreadState == PollingThread.State.Stopped)
            {
                throw new InvalidAsynchronousStateException("Already stopped");
            }

            TryStop();
        }
        public void TryStop()
        {
            this.DropModem();
            if (this.ThreadState != PollingThread.State.Stopped)
            {
                _eventLoop.TryStop();
            }
        }



        private void FinalizePendingForStatus()
        {

            _pendingMessagesWaitForStatus.Keys
                .ForEach(key =>
                             {
                                 SmsMessage msg;
                                 if (_pendingMessagesWaitForStatus.TryGetValue(key, out msg))
                                 {
                                     TimeSpan maxWait = Config.MaxWaitForAcknowledge;
                                     if (msg.Status >= 0) //Recieved report
                                     {
                                         var known = new MessageStatus((byte)msg.Status);
                                         if (known.IsKnownStatus() && known.Category != StatusCategory.TemporaryErrorWithRetry)
                                         {
                                             //no more reports expected
                                             if (known.Category == StatusCategory.Success)
                                             {
                                                 _pendingMessagesWaitForStatus.TryRemove(key, out msg);
                                                 _parent.SentMessages.Enqueue(msg);
                                             }
                                             else if (known.Category == StatusCategory.PermanentError)
                                             {
                                                 _pendingMessagesWaitForStatus.TryRemove(key, out msg);
                                                 _parent.FailedSendMessages.Enqueue(msg);
                                             }
                                             else if (known.Category == StatusCategory.TemporaryErrorNoRetry
                                                 && msg.SentAt.GetValueOrDefault().Add(maxWait) > DateTime.Now)
                                             {
                                                 _pendingMessagesWaitForStatus.TryRemove(key, out msg);
                                                 _parent.FailedSendMessages.Enqueue(msg);
                                             }
                                         }

                                     }
                                     else if (msg.Status < 0)
                                     {
                                         if (msg.Status == (int)SmsMessage.Statuses.SendFailed)
                                         {
                                             //Failed to send
                                             _pendingMessagesWaitForStatus.TryRemove(key, out msg);
                                             _parent.FailedSendMessages.Enqueue(msg);
                                         }
                                     }

                                     if (msg.SentAt.GetValueOrDefault().Add(maxWait) < DateTime.Now)
                                     {
                                         //Timing out
                                         _pendingMessagesWaitForStatus.TryRemove(key, out msg);
                                         this.Output("Timed out message to: " + msg.Number + "(" + msg.Reference + ") overdue:"
                                             + DateTime.Now.Subtract(msg.SentAt.GetValueOrDefault().Add(maxWait)).TotalMilliseconds);
                                         _parent.FailedSendMessages.Enqueue(msg);

                                     }
                                 }
                             });
        }

        public void EnqueueCommand(SmsMessage cmd)
        {
            this._incomingMessages[cmd.Priority].Enqueue(cmd);
        }

        public IdentificationInfo IdentifyPhone()
        {
            return this._comm.IdentifyDevice();
        }


        public IEnumerable<SmsMessage> TakeUnhandledCommands()
        {
            var result = new List<SmsMessage>();
            this._incomingMessages
                .PrioRange
                .ForEach(prio =>
                             {
                                 SmsMessage cmd;
                                 while (this._incomingMessages[prio].TryDequeue(out cmd))
                                 {
                                     result.Add(cmd);
                                 }
                             });
            return result;
        }

        public string SendUssdRequest(string request)
        {
            string data = TextDataConverter.StringTo7Bit(request);

            var asPDUencoded = Calc.IntToHex(TextDataConverter.SeptetsToOctetsInt(data));
            try
            {
                IProtocol protocol = _comm.GetProtocol();
                string gottenString = protocol.ExecAndReceiveMultiple("AT+CUSD=1," + asPDUencoded + ",15");
                var re = new Regex("\".*?\"");
                int i = 0;
                if (!re.IsMatch(gottenString))
                {
                    do
                    {
                        protocol.Receive(out gottenString);
                        ++i;
                    } while (!(i >= 5
                                || re.IsMatch(gottenString)
                                || gottenString.Contains("\r\nOK")
                                || gottenString.Contains("\r\nERROR")
                                || gottenString.Contains("\r\nDONE")));
                }
                string m = re.Match(gottenString).Value.Trim('"');
                return PduParts.Decode7BitText(Calc.HexToInt(m));
            }
            catch { }
            finally
            {
                _comm.ReleaseProtocol();
            }
            return "";
        }

        public static bool NeedUnicode(string message)
        {
            Encoding iso = Encoding.GetEncoding(1252);
            var isoBytes = iso.GetBytes(message);
            string isoString = iso.GetString(isoBytes);
            return isoString != message;
        }

        private void ShowException(Exception exception)
        {
            Debug.WriteLine(string.Format("({0}) Exception in sender:{1}", this._name, exception.Message));
        }

        private void Output(string p0)
        {
            string message = string.Format("({0}) {1}", this._name, p0);
            Report(this, message);
            Debug.WriteLine(message);
        }

        public void DropModem()
        {
            this.WaitForSendComplete();
            if (this._comm != null && this._comm.IsOpen())
            {
                this._comm.Close();
            }
            this._comm = null;
        }

        public void ConnectModem(string port, int baudRate = GsmCommMain.DefaultBaudRate, int timeout = GsmCommMain.DefaultTimeout)
        {
            if (this._comm != null && this._comm.IsConnected())
                this.DropModem();
            this._comm = null;
            this._comm = this.CreateGsmCommMain(port, baudRate, timeout);
            _phoneDisconnected = false;
        }

        private void WaitForSendComplete()
        {
            if (this.ThreadState == PollingThread.State.Running)
            {
                _eventLoop.TryStop();
            }
            this.Config.IsPaused = true;
            DateTime t1 = DateTime.Now.AddSeconds(30);
            while (this._waitingSendAckCounter > 0 && t1 > DateTime.Now)
            {
                Thread.Sleep(50);
            }
        }

        #region Implementation of IDisposable
        /// <summary>
        ///     Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            if (this._comm != null && this._comm.IsOpen())
            {
                this._comm.Close();
            }
            this._comm = null;
        }
        #endregion


    }
}